package AbstractMapper;

import java.util.ArrayList;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.TreeSet;

import org.opencyc.cycobject.CycConstant;
import org.opencyc.cycobject.CycList;
import org.opencyc.util.Log;

//TODO at some point: create an AbstractAnalogy class with its own scores. Make both the AbstractElementPair and the AbstractAnalogy implement Comparable and do the sorting within. Eliminate the scafolding required to keep scores by putting it inside the appropriate objects.
//This class should just create the necessary analogies and compare them. 

/**
 *Represents a series of analogies (each made up of a analog pairs) constructed out of cycConstant mappings. 
 *
 *@author Nic Di Noia - nik.ddn@gmail.com
 * 
 *
 */
public class AbstractAnalogyManager {

	protected CycConnection cycBridge;
	private CycList elements;
	private boolean printOnlyTopResult = true; //TODO set this as a menu option
	
	static final double SHORTEST_AVG_LENGTH = 9.5; //estimated to represent the Average Path Length between any two constants in the DB (shortest is assumed if many paths exist) 
													//Used in firstDistance z-score as the "mean of the population"	
													//TODO write down estimation formula
	/**
	 * An analogy construct made up of:
	 * One connection to cyc
	 * A list of elements.
	 * A list of pairs made up from those elements. 
	 * Four lists with mapping scores for each pair.
	 * @param cycC the connection to OpenCyc
	 */
	public AbstractAnalogyManager(CycConnection cycC)	
	{
		this.cycBridge = cycC;
		elements = new CycList();
	}
			
	/**
	 * add cyc constant to elements list.
	 * @param constant the constant to add.
	 */
	@SuppressWarnings("unchecked")
	public void addConstant(CycConstant constant)
	{	 elements.add(constant);	}
	
	
	/**
	 * Iterates through a list of mappings and asks them to calculate their scores.
	 * @param mappings the list of analogies (mapped pairs) to iterate through
	 */
	protected void calcScores(ArrayList<AbstractAnalogy> mappings) 
	{			
		for (int i = 0; i< mappings.size(); i++)
		{
			if (!mappings.get(i).calcRequiredScores())//calculates scores and returns false if failed		
				Log.current.println("ERROR - Could not calculate mapping scores for analogy " + mappings.get(i) + "\n");		
		}			
	}
	
	@SuppressWarnings("unchecked")
	protected boolean expandConceptsInAnalogs(CycList analog1, CycList analog2)
	{
		CycList generalizations1, generalizations2, isas1, isas2;
		generalizations1 = new CycList();
		generalizations2 = new CycList();
		isas1 = new CycList();
		isas2 = new CycList();	

		for (int i=0; i < analog1.size(); i++)			
			generalizations1.addAll(cycBridge.getGeneralizations((CycConstant)analog1.get(i)));			
		for (int i=0; i < analog1.size(); i++)			
			isas1.addAll(cycBridge.getIsa((CycConstant)analog1.get(i)));

		for (int i=0; i < analog2.size(); i++)			
			generalizations2.addAll(cycBridge.getGeneralizations((CycConstant)analog2.get(i)));
		for (int i=0; i < analog2.size(); i++)			
			isas2.addAll(cycBridge.getIsa((CycConstant)analog2.get(i)));

		CycConstant currentConst;
		CycList newAnalog1, newAnalog2;
		newAnalog1 = new CycList();		
		newAnalog2 = new CycList();
		CycList allConstants;

		if ((cycBridge.getExpandBy()==1)||(cycBridge.getExpandBy()>=3))
		{
			allConstants = new CycList(generalizations1.getReferencedConstants());//deep search for all constants (within cyclists, forts or narts)
			for (int i =0 ; i < allConstants.size(); i++)
			{	
				
				try			
				{
					currentConst = (CycConstant)allConstants.get(i);
				}
				catch (ClassCastException e)
				{
					currentConst = null;
				}
				if (currentConst!=null)
					newAnalog1.add(currentConst);				
			}
		}
		if (cycBridge.getExpandBy()>=2)
		{
			allConstants = new CycList(isas1.getReferencedConstants());
			for (int i =0 ; i < allConstants.size(); i++)
			{	
				try			
				{
				currentConst = (CycConstant)allConstants.get(i);
				}
				catch (ClassCastException e)
				{
					currentConst = null;
				}
				if (currentConst!=null)
					newAnalog1.add(currentConst);				
			}
		}
		if ((cycBridge.getExpandBy()==1)||(cycBridge.getExpandBy()>=3))
		{
			allConstants = new CycList(generalizations2.getReferencedConstants());
			for (int i =0 ; i < allConstants.size(); i++)
			{	
				try			
				{
					currentConst = (CycConstant)allConstants.get(i);
				}
				catch (ClassCastException e)
				{
					currentConst = null;
				}
				if (currentConst!=null)
					newAnalog2.add(currentConst);				
			}
		}
		if (cycBridge.getExpandBy()>=2)//Only add the isas if the generalizations didn't make the analog larger
		{
			allConstants = new CycList(isas2.getReferencedConstants());
			for (int i =0 ; i < allConstants.size(); i++)
			{	
				try			
				{
				currentConst = (CycConstant)allConstants.get(i);
				}
				catch (ClassCastException e)
				{
					currentConst = null;
				}
				if (currentConst!=null)
					newAnalog2.add(currentConst);				
			}
		}
		boolean reportPrep = false;
		if (!newAnalog1.isEmpty())
		{			
			if (cycBridge.getExpandBy()!=4)
				analog1.clear();
			//analog1.addAll(newAnalog1);
			analog1.addAll(newAnalog1.removeDuplicates());//remove any duplicates after expanding 
			reportPrep = true;
		}
		if (!newAnalog2.isEmpty())
		{
			if (cycBridge.getExpandBy()!=4)
				analog2.clear();
			//analog2.addAll(newAnalog2);
			analog2.addAll(newAnalog2.removeDuplicates()); //remove any duplicates after expanding
			reportPrep = true;
		}
		return reportPrep;
	}

	//********************************************************************************
	//PRINT METHODS START HERE *******************************************************
	//********************************************************************************
	/**
	 * Prints to the log file all of the pairing scores for pairs formed using {@link AbstractAnalogyManager#createAllPairMappings}
	 * <p>Sorts results using the preset sorting criteria. (@link AbstractMappingController#demo10}
	 * @see AbstractMappingController#demo12
	 */
	public void printAllPairScores()
	{		
		expl = new String();
		if (!elements.isEmpty())
		{
			String result = new String();			
			ArrayList<AbstractAnalogy> maps = this.createAllPairMappings();
			result += expl;
			if (maps!=null)
			{
				result+="\nElements used to create pairings: " + this.elements;			
				result+="\nSorted by: ";
				result+= CycConnection.reportSortBy(this.cycBridge.getSortBy());
				result+="\n";
				
				ArrayList<AbstractAnalogy> sortedResults;
				
				this.calcScores(maps);//populate scores for each mapping
				sortedResults = this.sortMappings(maps);
	
				Log.current.print(result);
				for (Iterator<AbstractAnalogy> it=sortedResults.iterator(); it.hasNext(); )
				{
					Log.current.print(it.next());
					if (printOnlyTopResult) break;
				}
			}
			else Log.current.println("ERROR - MAP CREATION FAILED!"); 

		}
		else
			Log.current.println("ERROR - NOTHING TO COMPARE");
				
	}

	/**
	 * Prints to the log file all of the mapping scores for analogs formed using {@link AbstractAnalogyManager#createMappingsFromEqualSizeAnalogs}
	 * <p>Sorts results using the preset sorting criteria. (@link AbstractMappingController#demo10}
	 * @see AbstractMappingController#demo13
	 */
	public void printSimilarityMappedAnalogy()
	{	
		expl = new String();
		
		if (elements.isEmpty())
			Log.current.println("ERROR - NOTHING TO COMPARE");
		else
			if (elements.size()%2 == 0)
			{
				String result = new String();			
				ArrayList<AbstractAnalogy> analogy = this.createMappingsFromEqualSizeAnalogs();	
				result += expl;
				if (analogy!=null)
				{
					result+="\nAnalog 1: ";
					for (int j = 0; j< elements.size()/2 ; j++)			
						result+=elements.get(j) + " ";
	
					result+="\nAnalog 2: ";
					for (int j = elements.size()/2; j< elements.size() ; j++)			
						result+=elements.get(j) + " ";
	
					result+="\nSorted by: ";
					result+= CycConnection.reportSortBy(this.cycBridge.getSortBy());	
					result+="\n";
	
					ArrayList<AbstractAnalogy> sortedResults;
					
					this.calcScores(analogy);//populate scores for each analogy
					sortedResults = this.sortMappings(analogy);
					Log.current.print(result);
					
					for (Iterator<AbstractAnalogy> it=sortedResults.iterator(); it.hasNext(); )
					{
						Log.current.print(it.next());
						if (printOnlyTopResult) break;
					}
					
				}
				else Log.current.println("ERROR - MAP CREATION FAILED!"); 
			}
			else
				Log.current.println("Score calculation for uneven analog pairs TBD");
	}	
	/**
	 * Prints to the log file all of the mapping scores for analogs formed using {@link AbstractAnalogyManager#createMappingsForSATAnalogs}
	 * <p>Sorts results using the preset sorting criteria. (@link AbstractMappingController#demo10}
	 * @see AbstractMappingController#demo14
	 */
	public void printSimMappedSATAnalogy()
	{	
		if (elements.size()<4)
			Log.current.println("ERROR - NOT ENOUGH ELEMENTS FOR PROPER SAT QUESTION");
		else
			if (elements.size()%2 == 0)
			{
				String result = new String();			
				ArrayList<AbstractAnalogy> maps = this.createMappingsForSATAnalogs();						
				if (maps!=null)
				{
					result+="\nSAT M/C ANALOGY QUESTION\n";
					for (int j = 0; j< 2; j++)			
						result+=elements.get(j) + " : ";
	
					result+=": ";
					for (int j = 2; j< elements.size() ; j++)
					{
						result+=elements.get(j) + " : ";
						if ((j%2!=0)&&(j!=elements.size()-1))
						{
							result+= "\n";
							for (int k = 0; k< 2; k++)			
								result+=elements.get(k) + " : ";
							result+= ": ";
						}
					}
					result+="\nSorted by: ";
					result+= CycConnection.reportSortBy(this.cycBridge.getSortBy());	
					result+="\n";
	
					ArrayList<AbstractAnalogy> sortedResults;				
					this.calcScores(maps);//populate scores for each analogy
					sortedResults = this.sortMappings(maps);
	
					Log.current.print(result);
					for (Iterator<AbstractAnalogy> it=sortedResults.iterator(); it.hasNext(); )
					{
						Log.current.print(it.next());
						if (printOnlyTopResult) break;
					}
					
				}
				else Log.current.println("ERROR - MAP CREATION FAILED!"); 
			}
			else
				Log.current.println("Score calculation for uneven analog pairs TBD");
	}

	protected String expl;//explanation written by createMappingsBasedOnStructure method
	@SuppressWarnings("unchecked")
	public void printDirectStructureMappedAnalogy()
	{
		expl = new String();
		//Log.current.println("ELEMENTS: " + this.elements);
		
		if (elements.isEmpty())
			Log.current.println("ERROR - NOTHING TO COMPARE");
		else
			if (elements.size() >= 3)//want at least 2 constants, and one element is null, the divider
			{
				int divIndex = -1;
				String result = new String();												
				//find the division between analogs
				for (int j = 0; j< elements.size(); j++)
				{
					if (elements.get(j)==null)					
						divIndex = j;
				}
				
				if ((divIndex ==0) || (divIndex == elements.size()-1))
					Log.current.println("ERROR - INCORRECTLY FORMED ANALOGS.");
				else
				{
					CycList analog1 = new CycList();
					CycList analog2 = new CycList();
					for (int i = 0; i < divIndex; i++)
					{
						analog1.add(elements.get(i)); //type is <CycConstant>
					}
					for (int i = divIndex+1; i < elements.size(); i++)
					{
						analog2.add(elements.get(i));
					}
					
					AbstractAnalogy analogy = DirectStructureMapper.createMappingsBasedOnSimpleStructure(this, analog1, analog2); 
					if (analogy!=null)
					{
						ArrayList<AbstractAnalogy> finalResult = new ArrayList<AbstractAnalogy>();
						finalResult.add(analogy);
						int oldReq = this.cycBridge.getRequiredScore();
//						this.cycBridge.setRequiredScore(0);
						this.calcScores(finalResult);		
//						this.cycBridge.setRequiredScore(oldReq);
						result+="\n\n"+ expl + "\n" + "Result:\n*******\n";
						result+="Analog 1: "+analog1 + "\n";
						result+="Analog 2: "+analog2 + "\n\n";
						result+=finalResult.get(0);
						Log.current.println(result);
					}
					else Log.current.println("ERROR - MAP CREATION FAILED!"); 
				}
			}
			else
				Log.current.println("ERROR - INCORRECTLY FORMED ANALOGS.");
	}

	//********************************************************************************
	//CREATE PAIRS/MAPPINGS METHODS START HERE ***************************************
	//********************************************************************************	

	/**
	 * @return list of CycConstant pairs - contains all possible pair combinations - n! pairs
	 */
	protected ArrayList <AbstractAnalogy> createAllPairMappings(CycList els)
	{
		if (els.size()>1)
		{
			ArrayList <AbstractAnalogy> list = new ArrayList<AbstractAnalogy> ();
			ArrayList <AbstractElementPair> pairInAShell; //used so that all create*pairing methods return an array of the same parameters.
			AbstractElementPair pair;
			AbstractAnalogy analogy; 
						
			int j;
			for (int i = 0; i < els.size(); i++)
			{
				for (j = i+1; j < els.size(); j++)
				{
					pair = new AbstractElementPair((CycConstant)els.get(i),(CycConstant)els.get(j),this.cycBridge);
					pairInAShell = new ArrayList<AbstractElementPair>();
					pairInAShell.add(pair);
					analogy = new AbstractAnalogy(this.cycBridge);
					analogy.populate(pairInAShell);
					list.add(analogy);
				}
			}
	
			return list;
		}
		else
			return null;
			
	}
	/**
	 * @return list of CycConstant pairs - uses owning object's elements CycList
	 */
	private ArrayList <AbstractAnalogy> createAllPairMappings()
	{
		return createAllPairMappings(this.elements);
	}

	/**
	 * Uses permutation generator to create all possible equal-size mappings from two analog lists(pairings of pairs).
	 * The analog lists are the result of bisecting the element list of this object.
	 * 
	 * @return a list containing all possible 1-1 mappings(lists of pairs) constructed out of two pair lists.
	 */
	protected ArrayList <AbstractAnalogy> createMappingsFromEqualSizeAnalogs(CycList els)
	{		
		if (els.size() % 2 != 0)
			return null;
		else
		{
			AbstractElementPair pair;
			Hashtable <String,AbstractElementPair> allPairs = new Hashtable <String,AbstractElementPair>();			
			ArrayList <String> oneAnalogyNames = new ArrayList<String>();
			ArrayList <AbstractAnalogy> analogies = new ArrayList<AbstractAnalogy>();
			ArrayList <CycConstant> analog1 = new ArrayList<CycConstant>();
			ArrayList <CycConstant> analog2 = new ArrayList<CycConstant>();
				
			int n;
			int[] indeces;
			PermutationGenerator perm;
			
			n = els.size()/2;
			
			//create a hashtable with each element in analog1 paired with every element from analog2 
			for (int i = 0; i < n; i++)
			{
				for (int j = n; j < els.size(); j++)
				{
					pair = new AbstractElementPair((CycConstant)els.get(i),(CycConstant)els.get(j),this.cycBridge);
					allPairs.put(pair.toString(), pair);
				}
			}
			this.cycBridge.setAllPairs(allPairs);
			this.cycBridge.scoresCalcd = false;//new pairs need new scores calculated.
			
			perm = new PermutationGenerator(n);
			while (perm.hasMore())
			{
				indeces = perm.getNext();
				for (int i = 0; i < n; i ++)														
					oneAnalogyNames.add(AbstractElementPair.toString((CycConstant)els.get(i), (CycConstant)els.get(indeces[i]+n)));					
				
				analogies.add(new AbstractAnalogy (oneAnalogyNames, this.cycBridge));//TODO FOR ANALOGS > 16 (tot n) MAKE NEW CLASS: ARRAYLIST OF ARRAYLISTS (EACH CAN ONLY HOLD INT.MAX_VALUE)
				oneAnalogyNames.clear();
				analog1.clear();
				analog2.clear();
			}

			return analogies; 
		}
	}
	private ArrayList <AbstractAnalogy> createMappingsFromEqualSizeAnalogs()
	{
		return createMappingsFromEqualSizeAnalogs(this.elements);
	}
	
	/**
	 * SAT mappings must be formed from an even number of constants, and there must be at least four of them.
	 * The mappings are formed between the first pair and every other element pair, preserving the pair order.
	 * Every possible mapping is formed between the first pair and all other pairs in the analogy.
	 * 
	 * @return a list of analogs for calculating SAT analogy scores (each analog is a list of pairs)
	 */
	private ArrayList <AbstractAnalogy> createMappingsForSATAnalogs()
	{
		AbstractElementPair pair, pair1;
		ArrayList <AbstractElementPair> oneMap = new ArrayList<AbstractElementPair>();
		ArrayList <AbstractAnalogy> analogies = new ArrayList<AbstractAnalogy>();
		if (elements.size() % 2 != 0)
			return null;
		else
		{
			for (int i = 2; i < elements.size()-1; i+=2 )
			{
				pair = new AbstractElementPair((CycConstant)elements.get(0),(CycConstant)elements.get(i), this.cycBridge);
				pair1 = new AbstractElementPair((CycConstant)elements.get(1),(CycConstant)elements.get(i+1), this.cycBridge);
				oneMap.add(pair);
				oneMap.add(pair1);
				if (!oneMap.isEmpty())
				{
					analogies.add(new AbstractAnalogy(this.cycBridge, new ArrayList<AbstractElementPair>(oneMap)));
					oneMap.clear();
				}
			}			

			return analogies; 
		}
	}
	
	//********************************************************************************
	//SORT/VOTE METHODS START HERE ***************************************
	//********************************************************************************
	
	/**
	 * Returns a sorted list of analogs according to the index set by {@link CycConnection#getSortBy()}. 
	 * 
	 * @return A sorted list of analogs; sorted according to the preset sort criteria. 
	 */
	protected ArrayList<AbstractAnalogy> sortMappings(ArrayList<AbstractAnalogy> analogies)
	{
		if (this.cycBridge.getSortBy() == 0)
			return this.voteOnMappings(analogies);
		else
		{			
			TreeSet<AbstractAnalogy> sortedResults = new TreeSet<AbstractAnalogy>(analogies);
			ArrayList<AbstractAnalogy> finalResults = new ArrayList<AbstractAnalogy>();
			
			for (Iterator<AbstractAnalogy> it=sortedResults.iterator(); it.hasNext(); )			
				finalResults.add(it.next());	
			
			return finalResults;
		}
	}
	/**
	 * Returns a sorted list of analogs according to the votes given by all the indices. {see @link CycConnection#getSortBy()}. Uses {@link AbstractAnalogyManager#sortMappings} as a helper. 
	 * 
	 * @return A sorted list of analogs; sorted according to the number of votes given by all sort criteria. 
	 */
	private ArrayList<AbstractAnalogy> voteOnMappings(ArrayList<AbstractAnalogy> maps)
	{		
		ArrayList <ArrayList<AbstractAnalogy>> results = new ArrayList<ArrayList<AbstractAnalogy>>();
		ArrayList <AbstractAnalogy> voter = new ArrayList<AbstractAnalogy>();
		int totVoters = CycConnection.totalVoters;
		if (cycBridge.getRequiredScore() >= 1)
			totVoters--;
		for (int i = 1; i <= totVoters ; i++)
		{
			this.cycBridge.setSortBy(i);
			voter = new ArrayList<AbstractAnalogy>(this.sortMappings(maps));//create a copy of the Analogies (a voter) and sort the mappings according to this voter's preference.
			results.add(voter);
		}//All votes are in -- the maps have been sorted by all types of scores
		
		Integer[] firstPlaceTally = this.tallyXplaceVotes(0, results);//call helper to tally the votes for 1st place
		Integer[] secondPlaceTally = null;
		Integer[] thirdPlaceTally = null;						
		
		if (results.get(0).size() >2)
			thirdPlaceTally = this.tallyXplaceVotes(2, results);//call helper to tally the votes for 3rd place
		
		if (results.get(0).size() > 1)//if we have more than one analogy candidate		
			secondPlaceTally = this.tallyXplaceVotes(1, results);//call helper to tally the votes for 2nd place										
			
		SortableResult current;
		ArrayList<SortableResult> sortResults = new ArrayList<SortableResult>();
		
		for (int y = 0; y < voter.size(); y++)		
		{
			if (results.get(0).size() > 2)
			{
			current = new SortableResult(firstPlaceTally[y],secondPlaceTally[y],thirdPlaceTally[y],results.get(0).get(y)); //create a new object pairing the analogy and its vote tally (for sorting)
			results.get(0).get(y).setVoteTally("[ " + firstPlaceTally[y] + ", " + secondPlaceTally[y] +", "+ thirdPlaceTally[y]+ " ]");//TODO fix this  temporary variable
			}
			else if (results.get(0).size() > 1)
			{
			current = new SortableResult(firstPlaceTally[y],secondPlaceTally[y],results.get(0).get(y)); //create a new object pairing the analogy and its vote tally (for sorting)
			results.get(0).get(y).setVoteTally("[ " + firstPlaceTally[y] + ", " + secondPlaceTally[y] + " ]");//TODO fix this  temporary variable
			}
			else
				{
				current = new SortableResult(firstPlaceTally[y],results.get(0).get(y));
				results.get(0).get(y).setVoteTally("[ " + firstPlaceTally[y] + " ]");//TODO fix this  temporary variable
				}
			
			sortResults.add(current);
		}//vote tallies are merged with each analogy and ready for comparison
		
		TreeSet<SortableResult> sortedResults = new TreeSet<SortableResult>(sortResults);
		ArrayList<AbstractAnalogy> finalResults = new ArrayList<AbstractAnalogy>();
		
		for (Iterator<SortableResult> it=sortedResults.iterator(); it.hasNext(); )			
			finalResults.add((AbstractAnalogy)it.next().getObject());	
		
		this.cycBridge.setSortBy(0); //last thing before return - set sort index back to 'voting method'		
		return finalResults;
	}
	//helper for voting method
	private Integer[] tallyXplaceVotes(int placeIndex, ArrayList <ArrayList<AbstractAnalogy>> results)
	{
		int BallotSize = results.get(0).size();	
		Integer[] placeTally = new Integer[BallotSize];
				
		placeTally[placeIndex] = 1;//initialize (ie. placeIndex has one vote from the 'base' voter)		
		for (int a = 0; a < BallotSize; a++)
		{
			if (a != placeIndex)//already been initialized
				placeTally[a] = 0;
		}//all votes are initialized		
		AbstractAnalogy	vote;		
				
		for (int j = 1; j < results.size() ; j++)
		{
			vote = results.get(j).get(placeIndex);
			
			for (int k = 0; k < BallotSize; k++)				
				if (results.get(0).get(k).equals(vote)) //find the base voter's candidate to sum the tally																														
					placeTally[k]++;																		
		}//index-place votes have been tallied
		return placeTally;
	}
	
}
